package service.validation;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.springframework.validation.Errors;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import service.configuration.IFieldTextConfig;
import entities.Status;

public class TextValidator {
	
	private final StringValidator stringValidator;
	
	// TODO : Les expression ne sont pas bonnes!
	private final String pathParagraph = "/root/p";
	private final String pathOrderedList = "//ol";
	private final String pathUnorderedList = "//ul";
	private final String pathList = "//ul|//ol";
	private final String pathListOfList = "//li/ul|//li/ol";
	private final String pathTable = "//table";
	
	private XPathExpression xpathParagraph;
	private XPathExpression xpathOrderedList;
	private XPathExpression xpathUnorderedList;
	private XPathExpression xpathList;
	private XPathExpression xpathListOfList;
	private XPathExpression xpathTable;
	
	private DocumentBuilder documentFactory;
	
	public TextValidator() {
		
		stringValidator = new StringValidator();
		
		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
		
		documentBuilderFactory.setValidating(false);
		
		try {
			documentFactory = documentBuilderFactory.newDocumentBuilder();
			documentFactory.setErrorHandler(null);
		} catch (ParserConfigurationException e) {
			System.err.println("ASOF Error : XML parser is not init so there are not text validation.");
			documentFactory = null;
		}
		
		XPathFactory xpathFactory = XPathFactory.newInstance();
		XPath xpath = xpathFactory.newXPath();
		
		try {
			xpathParagraph = xpath.compile(pathParagraph);
			xpathOrderedList = xpath.compile(pathOrderedList);
			xpathUnorderedList = xpath.compile(pathUnorderedList);
			xpathList = xpath.compile(pathList);
			xpathListOfList = xpath.compile(pathListOfList);
			xpathTable = xpath.compile(pathTable);
		} catch (XPathExpressionException e) {
			
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre minimum de paragraphes spécifiés dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMinPararaphLimit(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathParagraph.evaluate(value, XPathConstants.NODESET);
			return conf.getMinParagraph() != null 
			    && list.getLength() < conf.getMinParagraph();
		} catch (XPathExpressionException e) {
			return false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre maximun de paragraphes spécifiés dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMaxPararaphLimit(Document value, IFieldTextConfig conf)  {
		try {
			NodeList list = (NodeList) xpathParagraph.evaluate(value, XPathConstants.NODESET);
			return conf.getMaxParagraph() != null 
			    && list.getLength() > conf.getMaxParagraph();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte la contrainte que les tableaux ne sont pas 
	 * autorisé si cette contraintes spécifié dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean containsBadTable(Document value, IFieldTextConfig conf)  {
		try {
			NodeList list = (NodeList) xpathTable.evaluate(value, XPathConstants.NODESET);
			return ! conf.containsTable()  
			    && list.getLength() > 0;
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte la contrainte qu'une liste ne peut pas contenir de liste.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la contrainte est précisée et respectée, false sinon
	 */
	protected boolean constainsListOfList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathListOfList.evaluate(value, XPathConstants.NODESET);
			return list.getLength() > 0;
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre minimum de listes ordonnées 
	 * spécifiées dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMinOrderedList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathOrderedList.evaluate(value, XPathConstants.NODESET);
			return conf.getMinOrderedList() != null 
			    && list.getLength() < conf.getMinOrderedList();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre maximun de listes ordonnées 
	 * spécifiées dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMaxOrderedList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathOrderedList.evaluate(value, XPathConstants.NODESET);
			return conf.getMaxOrderedList() != null
			    && list.getLength() > conf.getMaxOrderedList();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre minimum de listes non ordonnées
	 * spécifiées dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMinUnorderedList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathUnorderedList.evaluate(value, XPathConstants.NODESET);
			return conf.getMinUnorderedList() != null
			    && list.getLength() < conf.getMinUnorderedList();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre maximun de listes non ordonnées
	 * spécifiées dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMaxUnorderedList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathUnorderedList.evaluate(value, XPathConstants.NODESET);
			return conf.getMaxUnorderedList() != null
			    && list.getLength() > conf.getMaxUnorderedList();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre minimum de listes spécifiées dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMinList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathList.evaluate(value, XPathConstants.NODESET);
			return conf.getMinList() != null
			    && list.getLength() < conf.getMinList();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Teste si la valeur respecte le nombre maximun de listes spécifiées dans la configuration.
	 * Cette méthode utilise des requêtes XPath.
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @return true si la limite est précisée et respectée, false sinon
	 */
	protected boolean exceedMaxList(Document value, IFieldTextConfig conf) {
		try {
			NodeList list = (NodeList) xpathList.evaluate(value, XPathConstants.NODESET);
			return conf.getMaxList() != null
			    && list.getLength() > conf.getMaxList();
		} catch (XPathExpressionException e) {
			return  false;
		}
	}
	
	/**
	 * Construit un document XML à partir de la chaine de caractère fournit en paramètre
	 * 
	 * @param value une chaine de caractère contenant un document XML ou une partie de document XML
	 * @return document XML
	 */
	public Document parseXML(String value) {
		try {
			value = "<root>" + value + "</root>";
			return documentFactory.parse(new ByteArrayInputStream(value.getBytes()));
		} catch (SAXException e) {
			return null;
		} catch (IOException e) {
			return null;
		}
	}
	
	/**
	 * Teste la validité de la valeur d'un champs de type texte (text).
	 * 
	 * @param value la valeur du champs à tester
	 * @param conf la configuration du champs de type texte (text)
	 * @return le status du champs SoftError ou Valid
	 */
	public Status validate(String value, IFieldTextConfig conf) {
		Status status;
		status = stringValidator.validate(value, conf);
		if (status == FieldValidator.statusError) {
			return status;
		}
		
		Document xmlvalue = parseXML(value);
		if (xmlvalue == null) {
			return FieldValidator.statusValid;
		}
		
		if (exceedMinPararaphLimit(xmlvalue, conf)
				|| exceedMaxPararaphLimit(xmlvalue, conf) 
				|| exceedMinOrderedList(xmlvalue, conf)
				|| exceedMinUnorderedList(xmlvalue, conf)
				|| exceedMinUnorderedList(xmlvalue, conf)
				|| containsBadTable(xmlvalue, conf)
				|| constainsListOfList(xmlvalue, conf)
				|| exceedMinList(xmlvalue, conf)
				|| exceedMaxList(xmlvalue, conf)) {
			return FieldValidator.statusError;
		}
		return stringValidator.validate(value, conf);
	}
	
	/**
	 * Teste la validité de la valeur d'un champs de type texte (text),
	 * et lie le champs à un message d'erreur en cas d'erreur.
	 * 
	 * @param name le nom du champs, util pour lier un message d'erreur au champs
	 * @param value la valeur du champs à tester
	 * @param conf la confgiguration du champs de type texte (text)
	 * @param errors mapping permettant de lier erreurs et messages
	 * @return le status du champs SoftError ou Valid
	 */
	public Status validate(String name, String value, IFieldTextConfig conf, Errors errors) {
		if (conf == null || value == null) {
			return FieldValidator.statusValid;
		}
		
		Status status;
		status = stringValidator.validate(name, value, conf, errors);
		if (status == FieldValidator.statusError) {
			return status;
		}
		
		
		Document xmlvalue = parseXML(value);
		if (xmlvalue == null) {
			return FieldValidator.statusValid;
		}
		
		
		if (containsBadTable(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.tablenotauthorized");
			return FieldValidator.statusError;
		}
		
		if (constainsListOfList(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.listoflistnotauthorized");
			return FieldValidator.statusError;
		}
		if (exceedMinList(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.notenoughlist");
		}
		if (exceedMaxList(xmlvalue, conf)) {
			if (conf.getMaxList() != null && conf.getMaxList() == 0) {
				errors.rejectValue(name, "error.field.text.listnotauthorized");
			} else {
				errors.rejectValue(name, "error.field.text.toomanylist");
			}
		}
		if (exceedMinOrderedList(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.notenoughorderedlist", 
					new Object[] {new Integer(conf.getMinOrderedList())}, "");
			return FieldValidator.statusError;
		}
		if (exceedMaxOrderedList(xmlvalue, conf)) {
			if (conf.getMaxOrderedList() != null && conf.getMaxOrderedList() == 0) {
				if (conf.getMaxUnorderedList() != null && conf.getMaxUnorderedList() == 0) {
					errors.rejectValue(name, "error.field.text.listnotauthorized");
				} else {
					errors.rejectValue(name, "error.field.text.listorderednotauthorized");
				}
			} else {
				errors.rejectValue(name, "error.field.text.toomanyorderedlist", 
						new Object[] {new Integer(conf.getMaxOrderedList())}, "");
			}
			return FieldValidator.statusError;
		}
		if (exceedMinUnorderedList(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.notenoughunorderedlist", 
					new Object[] {new Integer(conf.getMinUnorderedList())}, "");
			return FieldValidator.statusError;
		}
		if (exceedMaxUnorderedList(xmlvalue, conf)) {
			if (conf.getMaxUnorderedList() != null && conf.getMaxUnorderedList() == 0) {
				if (conf.getMaxOrderedList() != null && conf.getMaxOrderedList() == 0) {
					errors.rejectValue(name, "error.field.text.listnotauthorized");
				} else {
					errors.rejectValue(name, "error.field.text.listunorderednotauthorized");
				}
			} else {
				errors.rejectValue(name, "error.field.text.toomanyunorderedlist", 
						new Object[] {new Integer(conf.getMaxUnorderedList())}, "");
			}
			return FieldValidator.statusError;
		}

		if (exceedMinPararaphLimit(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.notenoughparagraph", 
					new Object[] {new Integer(conf.getMinParagraph())}, "");
			return FieldValidator.statusError;
		}
		if (exceedMaxPararaphLimit(xmlvalue, conf)) {
			errors.rejectValue(name, "error.field.text.toomanyparagraph", 
					new Object[] {new Integer(conf.getMaxParagraph())}, "");
			return FieldValidator.statusError;
		}
		
		return FieldValidator.statusValid;
	}
}
